package agents;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import agents.behaviours.GetDataBehaviour;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.Behaviour;
import jade.core.behaviours.CyclicBehaviour;
import jade.core.behaviours.OneShotBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.lang.acl.UnreadableException;
import jade.util.Logger;
import jade.wrapper.AgentContainer;
import weka.classifiers.bayes.NaiveBayesUpdateable;
import weka.core.Attribute;
import weka.core.FastVector;
import weka.core.FastVector.FastVectorEnumeration;
import weka.core.Instance;
import weka.core.Instances;

/** 
 * 
 * @author marcin szymon
 * 
 * Implementing and checking things required by agent-classifier project.
 * 
 * Start the right agent in the right container by editing Run Configurations
 * of EJADE Debug Agent and Deploy Agent.
 */
public class ClassifierAgent extends Agent {



	public void parseCommand(String command) {
		addBehaviour(new ParseCommandBehaviour(command));
	}
	
	protected void logMessage(String message) {
		if(mAgentLogger != null) {
			if (mAgentLogger.isLoggable(Logger.INFO)) {
				mAgentLogger.log(Logger.INFO, message);
			}		
		}
	}
	
	protected ArrayList<AID> searchYellowPages(String service) {
		// Find agent
		DFAgentDescription template = new DFAgentDescription();
		ServiceDescription sd = new ServiceDescription();
		sd.setType(service);
		template.addServices(sd);
		ArrayList<AID> agents = new ArrayList<AID>();
		
		try {
			DFAgentDescription[] result = DFService.search(this, template);
			for (DFAgentDescription ad : result) {
				if(!ad.getName().equals((getName()))){
					agents.add(ad.getName());					
				}
			}
		}
		catch (FIPAException fe) {
			fe.printStackTrace();
		}
		
		return agents; 
	}
	
	/**
	 * Get container name after moving to it.
	 * 
	 * Needed to read the right database fragment from file.
	 * Works - each container has its own "Console" after moving.
	 */
	protected void afterMove() {
		//Don't know if necessary
		//super.afterMove();
		
		// GUI
        mGui = new ClassifierAgentGui(this);
        mGui.showGui();
		
		AgentContainer ac = getContainerController();
		
//		try {
//			mCurrentContainer = ac.getContainerName();
//			
//		} catch (ControllerException e1) {
//			throw new RuntimeException(e1);
//		}
		
	}
	
	protected void beforeMove() {
		mGui.setVisible(false);
		mGui.dispose();
	}
	
	protected void setup() {
		
		 Object[] args = getArguments();
         if(args.length!=2){
                 throw new RuntimeException("Two arguments should be specified");
         }
         
         attributeName = args[0].toString();
         String classNames = args[1].toString();
 
         // Initialize logger
         mAgentLogger = Logger.getMyLogger(this.getClass().getName());
         logMessage(this.getName() + " Attribute: " + attributeName + ", Class: " + classNames);
         
         // Initialize weka
         mCModel = new NaiveBayesUpdateable();
         mAttr = new Attribute(attributeName);
         String[] _classNames = classNames.split(":");
         
         FastVector fvClassVal = new FastVector(_classNames.length);
         
         for (String c : _classNames) {
                 fvClassVal.addElement(c);
         }
         mAttr = new Attribute(attributeName,fvClassVal);

         
         if (mAgentLogger.isLoggable(Logger.INFO)) {
                 mAgentLogger.log(Logger.INFO, "TestLog");
         }               
         
         // Register the classification service in the yellow pages
         DFAgentDescription dfd = new DFAgentDescription();
         dfd.setName(getAID());
         ServiceDescription sd = new ServiceDescription();
         sd.setType(attributeName);
         sd.setName("Classification");
         dfd.addServices(sd);
         try {
                 DFService.register(this, dfd);
         }
         catch (FIPAException fe) {
                 fe.printStackTrace();
         }
         
         // GUI
         mGui = new ClassifierAgentGui(this);
         mGui.showGui();
         
         // Behaviours
         
         addBehaviour(new ServeClassification());
         addBehaviour(new GetDataBehaviour(this, mcGetDataAgentPeriod,attributeName));
	} 
	
	protected void takeDown() {
		try {
			DFService.deregister(this);
		}
		catch (FIPAException fe) {
			fe.printStackTrace();
		}
		mGui.dispose();
	}
	
	/**
	 * Updates model with new instances
	 * @param instances
	 */
	public void updateModel(Instances instances){
		try {

			//update class attribute with values from user
			for(int i = 0 ; i < instances.numAttributes();i++){
				if(instances.attribute(i).name().equals(attributeName)){
					instances.deleteAttributeAt(i);
					instances.insertAttributeAt(mAttr, i);
					break;
				}
			}
			instances.setClass(instances.attribute(attributeName));
			if(!modelBuilt){
				int classIndex = instances.classIndex();
				int counter = 0;
				mCModel.buildClassifier(instances);
				FastVectorEnumeration enume = (FastVectorEnumeration) instances.firstInstance().enumerateAttributes();
				attributes = new HashMap<String,Attribute>();
				while(enume.hasMoreElements()){
					if(counter == classIndex){
						attributeOrder.add(attributeName);
					}
					Attribute at = (Attribute)enume.nextElement();
					attributes.put(at.name(),at);
					attributeOrder.add(at.name());
					counter++;
				} 
				Attribute at = instances.attribute(attributeName);
				attributes.put(at.name(), at);
				mcDataSize = instances.numInstances();
				modelBuilt = true;
			}else{
				// update model
				Instance instance = instances.firstInstance();
				Enumeration<?> e = instance.enumerateAttributes();
				while(e.hasMoreElements()){
					Attribute at = (Attribute) e.nextElement();
					attributes.put(at.name(), updateAttributeValues(attributes.get(at.name()), at));
				}
				attributes.put(attributeName, updateAttributeValues(attributes.get(attributeName), instances.attribute(attributeName)));
				
				for (int i = 0 ; i< instances.numInstances();i++){
					try{
						mCModel.updateClassifier(instances.instance(i));						
					}catch(Exception e1){
						throw new RuntimeException(e1);
					}
					
					mcDataSize++;									
				} 
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

	}
	
	private Attribute updateAttributeValues(Attribute actualAttribute, Attribute newAttribute){
		Set<String> set = new HashSet<String>();
		FastVector fv = new FastVector();
		if(actualAttribute.isNominal()){			
			Enumeration<?> e = actualAttribute.enumerateValues();
			while (e.hasMoreElements()){
				String value = (String) e.nextElement();
				fv.addElement(value);
				set.add(value);
			}
			
			e = newAttribute.enumerateValues();
			while(e.hasMoreElements()){
				String value = (String) e.nextElement();
				if(!set.contains(value)){
					fv.addElement(value);				
				}
			}
			
			return new Attribute(actualAttribute.name(),fv);
		}else if(actualAttribute.isNumeric()){
			return actualAttribute;
		}
		else return null;
	}
	
	
	public TreeMap<String,Integer> distributionForInstance(TreeMap<String, String> attributes){
		if(modelBuilt){
			FastVector fv = new FastVector();
			Instance instance = new Instance(this.attributes.keySet().size());
			for(String key: attributeOrder){
				Attribute a = this.attributes.get(key);
				fv.addElement(a);				
			}
			
			Instances i= new Instances("REL", fv, 1);
			Attribute classAttribute = i.attribute(attributeName);
			
			i.setClass(classAttribute);
			
			instance.setDataset(i);
			
			for(String key : attributeOrder){
				Attribute a = this.attributes.get(key);
				String value = attributes.get(key);
				if(value == null || value.isEmpty()){
					instance.setMissing(a);
				}else{
					if(a.isNominal()){
						try{
							if(a.indexOfValue(value)!=-1){
								instance.setValue(a, value);																		
							}else{
								instance.setMissing(a);
							}
							
						}catch (Exception e){
							throw new RuntimeException(e);
						}
					}else if (a.isNumeric()){
						instance.setValue(a, Double.parseDouble(value));

					}else {
						throw new RuntimeException("Attribute of type " + a.type() + " not supported!");
					}
					
				}
			}
			try {
				double[] distribution = mCModel.distributionForInstance(instance);
				TreeMap<String, Integer> numberOfElementsInClass = new TreeMap<String,Integer>();
				Enumeration<?> e =classAttribute.enumerateValues();
				int counter = 0;
				while(e.hasMoreElements()){
					if(counter +1> distribution.length){
						throw new RuntimeException("Wrong distribution for instance");
					}
					numberOfElementsInClass.put((String)e.nextElement(),(int) (distribution[counter]*mcDataSize));
					
					counter++;
				}

				return numberOfElementsInClass;
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
			
			
		}
		
		return null;
		

	}
	
	
	
	//To jest cykliczne oczekiwanie na zapytania od inych agent�w 
	private class ServeClassification extends CyclicBehaviour {

		/**
		 * 
		 */
		private static final long serialVersionUID = 5264934762844176615L;

		@SuppressWarnings("unchecked")
		@Override
		public void action() {
			TreeMap<String, String> attributes;
			MessageTemplate mt = MessageTemplate.MatchPerformative(ACLMessage.CFP);
			ACLMessage msg = myAgent.receive(mt);
			if (msg != null) {
				// CFP Message received. Process it
					try {
						attributes =  (TreeMap<String, String>) msg.getContentObject();
						ACLMessage reply = msg.createReply();
						reply.setConversationId(attributeName);
						ClassifierAgent a = (ClassifierAgent) myAgent;
						TreeMap<String, Integer> distribution = a.distributionForInstance(attributes);
						if(distribution!=null){
							reply.setPerformative(ACLMessage.PROPOSE);
							reply.setContentObject(distribution);
						}else{
							reply.setPerformative(ACLMessage.REFUSE);
						}

						myAgent.send(reply);
					} catch (UnreadableException e) {
						throw new RuntimeException(e);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
			
			}
			else {
				block();
			}
		}
		
	}
	//GUI - user zadaje comende postaci (<nazwa-atryb>=<warto��>;...) i wysy�a
	private class ParseCommandBehaviour extends OneShotBehaviour {

		/**
		 * 
		 */
		private static final long serialVersionUID = -1749082979361039465L;

		ParseCommandBehaviour(String command) {
			super();
			mCommand = command;
		}
		
		@Override
		public void action() {
			
			// Parse command
			String attributes[] = mCommand.split(";");
			TreeMap<String, String> values = new TreeMap<String,String>();
			//mGui.setCommandReply("noa:" + String.valueOf(attributes.length) + ";");
			
			for (String attribute : attributes) {
				String pair[] = attribute.split("=");
				if (pair.length == 2) {
					values.put(pair[0], pair[1]);			
				} 
				else {
					mGui.setCommandReply(mGui.getReplyText() + "Err!;");
				}
			}
			List<AID> agents = new ArrayList<AID>();
			for(AID agent :searchYellowPages(attributeName)){
				agents.add(agent);
			}
			
			if (agents.isEmpty()) {
				mGui.setCommandReply(mGui.getReplyText() + "No agents;");
			} else {
				mGui.setCommandReply(mGui.getReplyText() + "Processing; ");
			}
			
			addBehaviour(new GlobalClassification(values,agents));
		}
		
		private String mCommand;
	}
	
	
	private class GlobalClassification extends Behaviour {
		
		/**
		 * 
		 */
		private static final long serialVersionUID = 466246869862808959L;
		private int repliesCnt = 0; 
		private int step = 0;
//		private ArrayList<AttributeQuery> mQueries = new ArrayList<AttributeQuery>();
//		private ArrayList<MessageTemplate> mMessageTemplates = new ArrayList<MessageTemplate>();
		
		private final List<AID>agents;
		
		private final TreeMap<String, String> attributes;
		
		private List<TreeMap<String, Integer>> replies = new ArrayList<TreeMap<String, Integer>>();
		
		
//		GlobalClassification(ArrayList<AttributeQuery> queries) {
//			super();
//			mQueries.addAll(queries);
//		}
		
		

		public GlobalClassification(TreeMap<String, String> values,
				List<AID> agents) {
			super();
			this.attributes = values;
			this.agents = agents;
		}

		@SuppressWarnings("unchecked")
		public void action() {
			switch (step) {
			case 0:
				// Send the cfp to all agents
				for(AID agentId :agents){
					
					ACLMessage cfp = new ACLMessage(ACLMessage.CFP);
					cfp.addReceiver(agentId);
					
					try {
						cfp.setContentObject(attributes);
					} catch (IOException e) {
						throw new RuntimeException(e);
					}
					
					cfp.setConversationId(attributeName);
					myAgent.send(cfp);
					
				}
			
				step = 1;
				break;
			case 1:
				// Receive all proposals/refusals from agents		
				MessageTemplate mt = MessageTemplate.MatchConversationId(attributeName);
				ACLMessage reply = myAgent.receive(mt);
					if (reply != null) {
						// Reply received
						if(reply.getPerformative() == ACLMessage.PROPOSE){
							try {
								replies.add((TreeMap<String, Integer>) reply.getContentObject());
							} catch (UnreadableException e) {
								throw new RuntimeException(e);
							}
						}
						
						
						mGui.setCommandReply(mGui.getReplyText() + reply.getSender().getName().split("@")[0]);		
						repliesCnt++; 
						if (repliesCnt >= agents.size()) {
							// We received all replies
							step = 2; 
						}
					}
					else {
						block();
					}

				break;
			case 2:
				classify();
				step = 3;
				
				break;
			}        
		}

		private void classify(){
			ClassifierAgent a =  (ClassifierAgent) myAgent;
			int totalSize = a.mcDataSize;
			TreeMap<String, Integer> myValues= a.distributionForInstance(attributes);
			if(myValues==null){
				myValues = new TreeMap<String, Integer> ();
			}
			//update values
			for(TreeMap<String, Integer> tm : replies ){
				for(String key: tm.keySet()){
					int value = tm.get(key);
					totalSize += value;
					if(myValues.containsKey(key)){
						int myValue = myValues.get(key);
						myValues.put(key, myValue+value);
					}else{
						myValues.put(key, value);
					}
//					if(myValue!=null){
//						myValues.put(key,myValue +value);					
//						
//					}
				}
			} 
			

			TreeMap<String,Float> probabilityMap = new TreeMap<String,Float>();
			for(String key : myValues.keySet()){
				probabilityMap.put(key, (float) (myValues.get(key)/(float)totalSize));
			}
			String className = "";
			Float probability = -1.0f;
			for(String key : probabilityMap.keySet()){
				Float val = probabilityMap.get(key);
				if(val>probability){
					probability = val;
					className = key;
				}
			}
			
			a.mGui.setCommandReply(className + " probability " + String.valueOf(probability));
			
		}
		
		
		public boolean done() {
			//if (step == 2 && bestSeller == null) {
			//	System.out.println("Attempt failed: "+targetBookTitle+" not available for sale");
			//}
			return (step == 3);
		}
	}  
	
	private Attribute mAttr;

	private boolean modelBuilt = false;
	
	private String attributeName;
	
	private Map<String, Attribute> attributes;
	
	private List<String> attributeOrder = new ArrayList<String>();
	
	private Logger mAgentLogger;
	
	
	private NaiveBayesUpdateable mCModel;

	private ClassifierAgentGui mGui;
	
	private static final long serialVersionUID = 321L;

	private int mcDataSize = 0;
	
	private static final long mcGetDataAgentPeriod = 1000;
}
